package com.example.demo;

import groovy.lang.Binding;
import groovy.lang.GroovyClassLoader;
import groovy.lang.Script;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import org.springframework.boot.autoconfigure.SpringBootApplication;

import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

@SpringBootApplication
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Benchmark)
@Fork(value = 2, jvmArgs = {"-Xms2G", "-Xmx2G"})
public class FormulaApplication {

    public static final int X_INTEGER = 1;
    public static final int Y_INTEGER = 2;

    public static final BigDecimal X_BigDecimal = new BigDecimal(1.1111);
    public static final BigDecimal Y_BigDecimal = new BigDecimal(2.2222);

    public static final String formula = "x * y";

    public static Script CompiledScript = null;

    static {
        try {
            CompiledScript = (Script) new GroovyClassLoader().parseClass(formula).newInstance();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(FormulaApplication.class.getSimpleName())
                .forks(1)
                .build();

        new Runner(opt).run();
    }

    @Benchmark
    public void testJavaCompiledCode_Integer() {
        Integer result = X_INTEGER * Y_INTEGER;
    }

    @Benchmark
    public void testJavaCompiledCode_BigDecimal() {
        BigDecimal result = X_BigDecimal.multiply(Y_BigDecimal);
    }

    @Benchmark
    public void testGroovyScript_Integer() throws IllegalAccessException, InstantiationException {
        Script script = (Script) new GroovyClassLoader().parseClass(formula).newInstance();
        Binding binding = script.getBinding();
        binding.setVariable("x", X_INTEGER);
        binding.setVariable("y", Y_INTEGER);
        Integer result = (Integer) script.run();
        assert result == 2;
    }

    @Benchmark
    public void testGroovyScript_BigDecimal() throws IllegalAccessException, InstantiationException {
        Script script = (Script) new GroovyClassLoader().parseClass(formula).newInstance();
        Binding binding = script.getBinding();
        binding.setVariable("x", X_BigDecimal);
        binding.setVariable("y", Y_BigDecimal);
        BigDecimal result = (BigDecimal) script.run();
        assert result.equals(new BigDecimal(2.46908642));
    }

    @Benchmark
    public void testCompiledGroovyScript_Integer() throws IllegalAccessException, InstantiationException {
        Binding binding = CompiledScript.getBinding();
        binding.setVariable("x", X_INTEGER);
        binding.setVariable("y", Y_INTEGER);
        Integer result = (Integer) CompiledScript.run();
        assert result == 2;
    }

    @Benchmark
    public void testCompiledGroovyScript_BigDecimal() throws IllegalAccessException, InstantiationException {
        Binding binding = CompiledScript.getBinding();
        binding.setVariable("x", X_BigDecimal);
        binding.setVariable("y", Y_BigDecimal);
        BigDecimal result = (BigDecimal) CompiledScript.run();
        assert result.equals(new BigDecimal(2.46908642));
    }

    @Benchmark
    public void testJavascriptInJava_Integer() throws ScriptException {
        Map<String, Object> map = new HashMap<>();
        map.put("x", X_INTEGER);
        map.put("y", Y_INTEGER);
        Integer result = (Integer) this.calculateResultByFormula(formula, map);
        assert result == 2;
    }

    @Benchmark
    public void testJavascriptInJava_BigDecimal() throws ScriptException {
        Map<String, Object> map = new HashMap<>();
        map.put("x", X_BigDecimal);
        map.put("y", Y_BigDecimal);
        Double result = (Double) this.calculateResultByFormula(formula, map);
        assert result.equals(new Double(2.46908642));
    }


    private Object calculateResultByFormula(String formula, Map<String, Object> paramMap) throws ScriptException {
        ScriptEngineManager manager = new ScriptEngineManager();

        ScriptEngine scriptEngine = manager.getEngineByName("js");

        for (String key : paramMap.keySet()) {
            formula = formula.replaceAll(key, paramMap.get(key).toString());
        }

        return scriptEngine.eval(formula);
    }
}
